import { UnlistenFn } from '@tauri-apps/api/event';
import { invoke } from "@tauri-apps/api/core";
import { listen } from '@tauri-apps/api/event';

export enum DataBits {
  Five = "Five",
  Six = "Six",
  Seven = "Seven",
  Eight = "Eight"
}

export enum FlowControl {
  None = "None",
  Software = "Software",
  Hardware = "Hardware"
}

export enum Parity {
  None = "None",
  Odd = "Odd",
  Even = "Even"
}

export enum StopBits {
  One = "One",
  Two = "Two"
}

export interface SerialportOptions {
  path: string;
  baudRate: number;
  encoding?: string;
  dataBits?: DataBits;
  flowControl?: FlowControl;
  parity?: Parity;
  stopBits?: StopBits;
  timeout?: number;
  size?: number;
  [key: string]: any;
}

export interface ReadDataResult {
  size: number;
  data: number[];
}

export interface ReadOptions {
  timeout?: number;
  size?: number;
}

export enum ClearBuffer {
  Input = "Input",
  Output = "Output",
  All = "All"
}

export const available_ports = async(): Promise<string[]>  =>  {
  try {
    const result = await invoke<string[]>("plugin:serial|available_ports");
    return Promise.resolve(result);
  } catch (error) {
    throw Promise.reject(error);
  }
}

export class SerialPort {
  option: SerialportOptions;
  isOpen: boolean;
  encoding: string;
  size: number;
  unListen?: UnlistenFn;

  constructor(option: SerialportOptions) {
    this.isOpen = false;
    this.encoding = option.encoding || "utf-8";
    this.option = {
      path: option.path,
      baudRate: option.baudRate,
      dataBits: option.dataBits || DataBits.Eight,
      flowControl: option.flowControl || FlowControl.None,
      parity: option.parity || Parity.None,
      stopBits: option.stopBits || StopBits.One,
      timeout: option.timeout || 100,
      size: option.size || 2048,
      encoding: option.encoding,
    }
    this.size = option.size || 2048;
  }

  setEncode(encoding: string) {
    this.encoding = encoding
    this.option.encoding = encoding
  }
  async open(): Promise<void> {
    try {
      if (!this.option.path) {
        return Promise.reject("Serialport path is empty");
      }
      if (!this.option.baudRate) {
        return Promise.reject("Serialport baudRate is empty");
      }
      if (this.isOpen) {
        return Promise.reject("Serialport is already open");
      }

      await invoke("plugin:serial|open", {
        path: this.option.path,
        baudRate: this.option.baudRate,
        dataBits: this.option.dataBits,
        flowControl: this.option.flowControl,
        parity: this.option.parity,
        stopBits: this.option.stopBits,
        timeout: this.option.timeout,
      });
      this.isOpen = true;
    }
    catch (error) {
      throw Promise.reject(error);
    }
  }
  async closeAll() {
    try {
      await invoke("plugin:serial|close_all");
    } catch (error) {
      throw Promise.reject(error);
    }
  }
  async cancelListen(): Promise<void> {
    try {
      if (this.unListen) {
        this.unListen();
        this.unListen = undefined;
      }
      return;
    } catch (error) {
      return Promise.reject('Failed to cancel serial monitoring: ' + error);
    }
  }

  async cancelRead(): Promise<void> {
    try {
      return await invoke<void>('plugin:serial|cancel_read', {
        path: this.option.path,
      });
    } catch (error) {
      return Promise.reject(error);
    }
  }

  async listen(fn: (...args: any[]) => void): Promise<void> {
    try {
      await this.cancelListen();
      let sub_path = this.option.path?.toString().replaceAll(".", "-").replaceAll("/", "-")
      let readEvent = `plugin-serial-read-${sub_path}`;
      console.log('listen event: ' + readEvent)
      this.unListen = await listen<ReadDataResult>(
        readEvent,
        ({ payload }) => {
          try {
            if (this.encoding !== 'none') {
              const decoder = new TextDecoder(this.encoding);
              const data = decoder.decode(new Uint8Array(payload.data));
              fn(data);
            } else {
              fn(new Uint8Array(payload.data));
            }
          } catch (error) {
            console.error(error);
          }
        },
      );
      return;
    } catch (error) {
      return Promise.reject('Failed to monitor serial port data: ' + error);
    }
  }


  async startListening(): Promise<void> {
    try {
      await invoke<string>('plugin:serial|start_listening', {
        path: this.option.path,
        size: this.option.size,
        timeout: this.option.timeout,
      });
    } catch (error) {
      return Promise.reject(error);
    }
  }

  async stopListening(): Promise<void> {
    try {
      await invoke<string>('plugin:serial|stop_listening', {
        path: this.option.path,
      });
    } catch (error) {
      return Promise.reject(error);
    }
  }


  async forceClose(path: string): Promise<void> {
    return await invoke<void>('plugin:serial|force_close', { path });
  }

  async close(): Promise<void> {
    try {
      if (!this.isOpen) {
        return;
      }
      await this.cancelRead();
      let res = undefined;
      res = await invoke<void>('plugin:serial|close', {
        path: this.option.path,
      })
      await this.cancelListen();
      this.isOpen = false;
      return res;
    } catch (error) {
      return Promise.reject(error);
    }
  }

  async read(options?: ReadOptions): Promise<string> {
    try {
      return await invoke<string>('plugin:serial|read', {
        path: this.option.path,
        timeout: options?.timeout || this.option.timeout,
        size: options?.size || this.size,
      });
    } catch (error) {
      return Promise.reject(error);
    }
  }

  async write(value: string): Promise<number> {
    try {
      if (!this.isOpen) {
        return Promise.reject(`serial port ${this.option.path} not opened!`);
      }
      return await invoke<number>('plugin:serial|write', {
        value,
        path: this.option.path,
      });
    } catch (error) {
      return Promise.reject(error);
    }
  }

  async writeBinary(value: Uint8Array | number[]): Promise<number> {
    try {
      if (!this.isOpen) {
        return Promise.reject(`serial port ${this.option.path} not opened!`);
      }
      if (value instanceof Uint8Array || value instanceof Array) {
        return await invoke<number>('plugin:serial|write_binary', {
          value: Array.from(value),
          path: this.option.path,
        });
      } else {
        return Promise.reject(
          'value Argument type error! Expected type: string, Uint8Array, number[]',
        );
      }
    } catch (error) {
      return Promise.reject(error);
    }
  }

  async setDataBits(value: DataBits): Promise<void> {
    try {
      return await invoke<void>('plugin:serial|set_data_bits', {
        path: this.option.path,
        dataBits: value
      });
    } catch (error) {
      return Promise.reject(error);
    }
  }

  async setBaudRate(value: number): Promise<void> {
    try {
      return await invoke<void>('plugin:serial|set_baud_rate', {
        path: this.option.path,
        baudRate: value
      });
    } catch (error) {
      return Promise.reject(error);
    }
  }

  async setParity(value: Parity): Promise<void> {
    try {
      return await invoke<void>('plugin:serial|set_parity', {
        path: this.option.path,
        parity: value
      });
    } catch (error) {
      return Promise.reject(error);
    }
  }

  async setStopBits(value: StopBits): Promise<void> {
    try {
      return await invoke<void>('plugin:serial|set_stop_bits', {
        path: this.option.path,
        stopBits: value
      });
    } catch (error) {
      return Promise.reject(error);
    }
  }

  async setRequestToSend(value: boolean): Promise<void> {
    try {
      return await invoke<void>('plugin:serial|write_request_to_send', {
        path: this.option.path,
        level: value
      });
    } catch (error) {
      return Promise.reject(error);
    }
  }

  /**
   * @description Sets the DTR (Data Terminal Ready) control signal
   * @param {boolean} value The signal level to set
   * @returns {Promise<void>} A promise that resolves when DTR is set
   */
  async setDataTerminalReady(value: boolean): Promise<void> {
    try {
      return await invoke<void>('plugin:serial|write_data_terminal_ready', {
        path: this.option.path,
        level: value
      });
    } catch (error) {
      return Promise.reject(error);
    }
  }

  /**
   * @description Reads the CTS (Clear To Send) control signal state
   * @returns {Promise<boolean>} A promise that resolves to the CTS state
   */
  async readClearToSend(): Promise<boolean> {
    try {
      return await invoke<boolean>('plugin:serial|read_clear_to_send', {
        path: this.option.path
      });
    } catch (error) {
      return Promise.reject(error);
    }
  }

  /**
   * @description Reads the DSR (Data Set Ready) control signal state
   * @returns {Promise<boolean>} A promise that resolves to the DSR state
   */
  async readDataSetReady(): Promise<boolean> {
    try {
      return await invoke<boolean>('plugin:serial|read_data_set_ready', {
        path: this.option.path
      });
    } catch (error) {
      return Promise.reject(error);
    }
  }

  /**
   * @description Reads the RI (Ring Indicator) control signal state
   * @returns {Promise<boolean>} A promise that resolves to the RI state
   */
  async readRingIndicator(): Promise<boolean> {
    try {
      return await invoke<boolean>('plugin:serial|read_ring_indicator', {
        path: this.option.path
      });
    } catch (error) {
      return Promise.reject(error);
    }
  }

  /**
   * @description Reads the CD (Carrier Detect) control signal state
   * @returns {Promise<boolean>} A promise that resolves to the CD state
   */
  async readCarrierDetect(): Promise<boolean> {
    try {
      return await invoke<boolean>('plugin:serial|read_carrier_detect', {
        path: this.option.path
      });
    } catch (error) {
      return Promise.reject(error);
    }
  }

  /**
   * @description Gets the number of bytes available to read
   * @returns {Promise<number>} A promise that resolves to the number of bytes
   */
  async bytesToRead(): Promise<number> {
    try {
      return await invoke<number>('plugin:serial|bytes_to_read', {
        path: this.option.path
      });
    } catch (error) {
      return Promise.reject(error);
    }
  }

  /**
   * @description Gets the number of bytes waiting to be written
   * @returns {Promise<number>} A promise that resolves to the number of bytes
   */
  async bytesToWrite(): Promise<number> {
    try {
      return await invoke<number>('plugin:serial|bytes_to_write', {
        path: this.option.path
      });
    } catch (error) {
      return Promise.reject(error);
    }
  }

  /**
   * @description Clears the specified buffer
   * @param {ClearBuffer} buffer The buffer to clear
   * @returns {Promise<void>} A promise that resolves when the buffer is cleared
   */
  async clearBuffer(buffer: ClearBuffer): Promise<void> {
    try {
      return await invoke<void>('plugin:serial|clear_buffer', {
        path: this.option.path,
        bufferToClear: buffer
      });
    } catch (error) {
      return Promise.reject(error);
    }
  }

  /**
   * @description Starts transmitting a break signal
   * @returns {Promise<void>} A promise that resolves when break signal starts
   */
  async setBreak(): Promise<void> {
    try {
      return await invoke<void>('plugin:serial|set_break', {
        path: this.option.path
      });
    } catch (error) {
      return Promise.reject(error);
    }
  }

  /**
   * @description Stops transmitting a break signal
   * @returns {Promise<void>} A promise that resolves when break signal stops
   */
  async clearBreak(): Promise<void> {
    try {
      return await invoke<void>('plugin:serial|clear_break', {
        path: this.option.path
      });
    } catch (error) {
      return Promise.reject(error);
    }
  }


}

